﻿using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace Vcm
{
    public class LibraryCardMaker : CardMakerBase
    {
        public override bool SupportsRow(DataRow cardRow)
        {
            try
            {
                var card = new LibraryCardData(cardRow);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public override void AddCard(ProxyDocument proxyDocument, DataRow cardRow, out string outputMessage)
        {
            var card = new LibraryCardData(cardRow);

            var doc = proxyDocument.Document;
            var writer = proxyDocument.Writer;

            var outputMessageBuilder = new StringBuilder();

            outputMessageBuilder.AppendLine("Processing card " + card.Name);

            while (--card.Copies >= 0)
            {
                float x, y;
                GetNewCardsCoordinates(proxyDocument, doc, out x, out y);

                var cb = writer.DirectContent;

                EncounteredClanNames.Clear();
                CardName = card.CorrectedName;

                var xBorderImage = x - (CardWidthBorder - CardWidth) / 2f;
                var yBorderImage = y - (CardHeightBorder - CardHeight) / 2f;

                var xArtImage = x + CardWidth * 0.1712f;
                var yArtImage = y + CardHeight * 0.424f;
                const float wArtImage = CardWidth * 0.8095f;
                const float hArtImage = CardHeight * 0.49475f;

                if (proxyDocument.UseGhost)
                {
                    cb.SetLineWidth(1f);
                    cb.SetRGBColorStroke(0, 0, 0);
                    cb.Rectangle(xBorderImage, yBorderImage, CardWidthBorder, CardHeightBorder);
                    cb.Stroke();
                    cb.Rectangle(xArtImage, yArtImage, wArtImage, hArtImage);
                    cb.Stroke();
                    cb.Rectangle(x, y, CardWidth, CardHeight);
                    cb.Stroke();
                }
                else
                {
                    // draw the black border by stretching a black box to the desired size
                    var borderImage = ImageProvider.GetAbsoluteSizedImage(">blackbox", string.Format(@"{0}Images\{1}", ResourcesPath, "blackbox"), ImageExtensions, CardWidthBorder, CardHeightBorder);
                    borderImage.SetAbsolutePosition(xBorderImage, yBorderImage);
                    doc.Add(borderImage);

                    // draw the card art
                    var artImagePath = String.Format("{0}\\{1}", proxyDocument.ArtPath, NormalizeImageName(card.Name));
                    var artImage = ImageProvider.GetAbsoluteSizedImage(NormalizeImageName(card.Name), artImagePath, ImageExtensions, wArtImage, hArtImage);
                    if (artImage != null)
                    {
                        var noArtImage = ImageProvider.GetAbsoluteSizedImage(">whitebox", string.Format(@"{0}Images\{1}", ResourcesPath, "whitebox"), ImageExtensions, wArtImage, hArtImage);
                        noArtImage.SetAbsolutePosition(xArtImage, yArtImage);
                        doc.Add(noArtImage);
                        artImage.SetAbsolutePosition(xArtImage + (wArtImage - artImage.ScaledWidth) / 2, yArtImage + (hArtImage - artImage.ScaledHeight) / 2);
                        doc.Add(artImage);
                    }
                    else
                    {
                        outputMessageBuilder.AppendLine("<!> Couldn't find artwork file " + artImagePath + " with any of these extensions: " + string.Join(", ", ImageExtensions));
                        // to save ink, draw a white box
                        var noArtImage = ImageProvider.GetAbsoluteSizedImage(">whitebox", string.Format(@"{0}Images\{1}", ResourcesPath, "whitebox"), ImageExtensions, wArtImage, hArtImage);
                        noArtImage.SetAbsolutePosition(xArtImage, yArtImage);
                        doc.Add(noArtImage);
                    }

                    // draw the card background
                    var backgroundName = card.Type.Split('/')[0].Trim();
                    var backgroundImage = ImageProvider.GetAbsoluteSizedImage(">background-" + backgroundName, string.Format(@"{0}Images\{1}", ResourcesPath, backgroundName), ImageExtensions, CardWidth, CardHeight);
                    //var backgroundImage = ImageProvider.GetAbsoluteSizedImage("4th", string.Format(@"{0}Images\{1}", ResourcesPath, "fourth"), ImageExtensions, CardWidth, CardHeight);
                    if (backgroundImage == null)
                        throw new Exception(string.Format("Error in card {0}, background type '{1}' not found", card.Name, backgroundName));
                    backgroundImage.SetAbsolutePosition(x, y);
                    doc.Add(backgroundImage);
                }

                // prepare the card name position
                var xName = x + CardWidth * 0.0265f;
                var yName = y + CardHeight * 0.94f;
                var wName = CardWidth * 0.9f;

                // write the expansion symbol
                if (proxyDocument.ExpansionSymbolPath != null)
                {
                    const float maxWidth = CardWidth * 0.07f;
                    const float maxHeight = CardHeight * 0.055f;
                    var xExpansionSymbol = x + CardWidth * .905f;
                    var yExpansionSymbol = y + CardHeight * .93f;

                    // debug expansion symbol outline
                    //cb.SetRGBColorStroke(255, 0, 0);
                    //cb.Rectangle(xExpansionSymbol, yExpansionSymbol, maxWidth, maxHeight);
                    //cb.Stroke();

                    var expansionSymbolImage = ImageProvider.GetFittingImage(">expansionSymbol", proxyDocument.ExpansionSymbolPath, maxWidth, maxHeight);
                    xExpansionSymbol += (maxWidth - expansionSymbolImage.ScaledWidth) / 2f;
                    yExpansionSymbol += (maxHeight - expansionSymbolImage.ScaledHeight) / 2f;

                    expansionSymbolImage.SetAbsolutePosition(xExpansionSymbol, yExpansionSymbol);
                    doc.Add(expansionSymbolImage);
                    wName = xExpansionSymbol - xName;
                }

                // write the card name
                var nameFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\matrixb.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
                var fonts = new BaseFont[7];
                fonts[(int)FontType.Regular] = nameFont;

                WriteName(doc, cb, card.CorrectedName, xName, yName, wName, 12, fonts, proxyDocument.UseGhost ? 0 : 0xFFFFFF);

                // write the card type icon(s)
                var xIcon = x + CardWidth * 0.028f;
                var yIcon = y + CardHeight * 0.81f;

                var cardtypes = card.Type.Split('/').Where(ct => !string.IsNullOrEmpty(ct)).ToList();

                IconInfo iconInfo = null;
                string iconsText = String.Empty;
                const int symbolSize = 19;
                string additionalText = null;
                var symbolSizes = new List<float>();
                var verticalOffsets = new List<float>();
                foreach (var cardtype in cardtypes)
                {
                    if (Icons.TryGetValue(String.Format("[{0}]", cardtype.Trim()), out iconInfo))
                    {
                        iconsText += iconInfo.Text;
                        symbolSizes.Add(symbolSize * iconInfo.ScaleFactor);
                        verticalOffsets.Add(iconInfo.VerticalOffset);
                        if (iconInfo.AdditionalText != null)
                        {
                            additionalText = iconInfo.AdditionalText;
                            symbolSizes.Add(symbolSize * iconInfo.ScaleFactor);
                            verticalOffsets.Add(iconInfo.VerticalOffset);
                        }
                    }
                }
                if (cardtypes.Count == 1 && iconInfo != null && iconInfo.IsAlsoAction)
                {
                    var actionIconInfo = Icons["[ACTION]"];
                    iconsText = actionIconInfo.Text + iconsText;
                    symbolSizes.Insert(0, symbolSize * actionIconInfo.ScaleFactor);
                    verticalOffsets.Insert(0, actionIconInfo.VerticalOffset);
                }

                for (var i = 0; i < iconsText.Length / 2; ++i)
                {
                    yIcon += verticalOffsets[i] * symbolSizes[i];
                    WriteSymbol(cb, xIcon, yIcon, SymbolFont, symbolSizes[i], iconsText[i * 2], iconsText[i * 2 + 1]);
                    yIcon -= symbolSizes[i] * .95f;
                }

                const float additionalTextFontSize = 5f;
                if (additionalText != null)
                {
                    WriteTextCentered(cb, additionalText, xIcon + symbolSize * .4f, yIcon + symbolSizes.Last() * .95f - additionalTextFontSize * 1.7f, TextFont, additionalTextFontSize, 0xFFFFFF);
                }

                // write the card clan (currently using bitmap)
                var xClanSymbol = x + CardWidth * 0.075f;
                var yClanSymbol = y + CardHeight * 0.617f;

                var clanSymbolLayouts = new List<ClanSymbolLayout>(2);
                bool firstClanSymbol = true; // the first clan symbol is centered vertically

                // first, gather informations before displaying the symbols
                // because we want the largest symbols to be displayed last (so that the seal wax covers the other symbols)
                var cardClans = card.Clan.Split('/').Select(c => c.Trim()).Where(cc => !string.IsNullOrEmpty(cc)).ToList();
                foreach (var cardClan in cardClans)
                {
                    var clanSymbolInfo = ClanWidthFactors.ValueOrDefault(cardClan, DefaultClanSymbolInfo);

                    var wClanSymbol = CardWidth * clanSymbolInfo.WidthFactor;
                    var cardClanSymbol = ImageProvider.GetFittingImage("clanSymbol-" + cardClan, string.Format(@"{0}Images\ClanSymbol-{1}", ResourcesPath, cardClan), ImageExtensions, wClanSymbol, CardHeight);
                    if (cardClanSymbol == null)
                        continue;

                    clanSymbolLayouts.Add(new ClanSymbolLayout
                    {
                        Width = wClanSymbol,
                        ClanSymbol = cardClanSymbol,
                        IsVerticallyCentered = firstClanSymbol && !clanSymbolInfo.IsRotated, // rotated symbols are never centered
                        X = xClanSymbol - wClanSymbol / 2f
                    });
                    firstClanSymbol = false;
                }

                if (clanSymbolLayouts.Count > 0)
                {
                    // compute the clan symbol vertical positions
                    int i = 0;
                    while (true)
                    {
                        var clanSymbolLayout = clanSymbolLayouts[i];

                        var hClanSymbol = clanSymbolLayout.ClanSymbol.ScaledHeight;
                        yClanSymbol -= clanSymbolLayout.IsVerticallyCentered ? hClanSymbol / 2f : hClanSymbol;
                        if (i == 0 && !clanSymbolLayout.IsVerticallyCentered) // the first symbol wasn't centered => it must be bigger (eg. Assamite) and therefore drawn a little higher.
                            yClanSymbol += hClanSymbol / 4f;
                        clanSymbolLayout.Y = yClanSymbol;

                        if (++i == clanSymbolLayouts.Count)
                            break;

                        if ((clanSymbolLayout.X >= x) && (clanSymbolLayouts[i].X >= x)) // two symbols without wax seal
                            yClanSymbol -= CardHeight * 0.016f; // space between two clan symbols
                        else // one of the two symbols at least have a wax seal
                            yClanSymbol += CardHeight * 0.009f; // one symbol must be slighly over the other
                    }

                    // draw the clan symbols, ordered by increasing width so that the wax seal symbols go on top
                    foreach (var clanSymbolLayout in clanSymbolLayouts.OrderBy(csl => csl.Width))
                    {
                        clanSymbolLayout.ClanSymbol.SetAbsolutePosition(clanSymbolLayout.X, clanSymbolLayout.Y);
                        doc.Add(clanSymbolLayout.ClanSymbol);
                    }
                }

                // write the card disciplines (and the burn option if any)
                var xDiscipline = x + CardWidth * 0.028f;
                var yDiscipline = y + CardHeight * 0.51f;
                const int disciplineSize = 19;

                if (clanSymbolLayouts.Count > 0)
                {
                    var lastClanSymbolLayout = clanSymbolLayouts[clanSymbolLayouts.Count - 1];
                    var bottom = lastClanSymbolLayout.Y;

                    if (yDiscipline + disciplineSize > bottom)
                        yDiscipline = bottom - disciplineSize * BaselineFactor; // remove font baseline
                }

                var disciplines = card.Discipline.Split('/', '&');

                // draw the burn option
                var disciplinesText = card.HasBurnOption
                    ? Icons["[BURN OPTION]"].Text
                    : String.Empty;

                foreach (var discipline in disciplines.Select(di => di.Trim()))
                {
                    var disciplineInfo = Disciplines.Values.FirstOrDefault(di => string.Equals(di.Name, discipline, StringComparison.InvariantCultureIgnoreCase));
                    if (disciplineInfo != null)
                    {
                        disciplinesText += disciplineInfo.Text.Length == 1
                            ? InferiorDiscipline + disciplineInfo.Text
                            : disciplineInfo.Text;
                    }
                }

                for (var i = 0; i < disciplinesText.Length / 2; ++i)
                    WriteSymbol(cb, xDiscipline, yDiscipline - i * disciplineSize, SymbolFont, disciplineSize, disciplinesText[i * 2], disciplinesText[i * 2 + 1]);

                // write the sphere text
                if (card.Sphere.Length > 0)
                {
                    const float wSphere = CardWidth * 0.803f;

                    var xSphere = x + CardWidth * 0.176f;
                    var ySphere = y + CardHeight * 0.374f;

                    //cb.SetRGBColorStroke(255, 0, 0);
                    //cb.Rectangle(xSphere, ySphere, sphereWidth, 9);
                    //cb.Stroke();

                    WriteOneLine(doc, cb, card.Sphere, xSphere, ySphere, wSphere, 12f, fonts, proxyDocument.UseGhost ? 0 : 0xFFFFFF, 0x000000, isCentered: true);
                }

                // write the card text
                const float wTextBox = CardWidth * 0.743f;
                const float hTextBox = CardHeight * 0.324f;

                var xTextBox = x + CardWidth * 0.206f;
                var yTextBox = y + CardHeight * 0.04f;

                fonts[(int)FontType.Regular] = TextFont;
                fonts[(int)FontType.Bold] = BoldTextFont;
                fonts[(int)FontType.Italic] = ItalicTextFont;
                fonts[(int)FontType.BoldItalic] = BoldItalicTextFont;
                fonts[(int)FontType.Symbol] = SymbolFont;
                fonts[(int)FontType.Condensed] = CondensedFont;

                // for cards with a capacity circle
                var capacityXCenter = x + CardWidth * 0.935f;
                var capacityYCenter = y + CardHeight * 0.058f;
                const float capacityRadius = CardWidth * 0.05f;

                // debug text box outline
                //cb.SetRGBColorStroke(255, 0, 0);
                //cb.Rectangle(xTextBox, yTextBox, textWidth, textHeight);
                //cb.Stroke();

                // write the watermark
                if (!string.IsNullOrEmpty(proxyDocument.Watermark))
                {
                    bool overArt = proxyDocument.WatermarkPosition == CardPosition.ArtBox;
                    WriteWatermark(cb, proxyDocument.Watermark, overArt ? xArtImage : xTextBox, overArt ? yArtImage : yTextBox,
                        overArt ? wArtImage : wTextBox, overArt ? hArtImage : hTextBox, nameFont, proxyDocument.UseGhost, proxyDocument.WatermarkOrientation, proxyDocument.WatermarkColor, proxyDocument.WatermarkOpacity);
                }

                if (proxyDocument.UseGhost && card.HasCapacity)
                {
                    cb.SetLineWidth(1f);
                    cb.SetRGBColorStroke(0, 0, 0);
                    //cb.Rectangle(capacityXCenter - 1, capacityYCenter - 1, 2, 2);
                    cb.Circle(capacityXCenter, capacityYCenter, capacityRadius);
                    cb.Stroke();
                }

                // add some exclusion functions so that the box of a word doesn't hit the capacity circle.
                var symetricalCapacityXCenter = xTextBox - (capacityXCenter - (xTextBox + wTextBox));
                var exclusionAreaFuncs = !card.HasCapacity
                    ? null
                    : new List<Func<Rectangle, bool, bool>>
                        {
                            (r, isSymetrical) =>
                                {
                                    bool result = Intersects(capacityXCenter, capacityYCenter, capacityRadius, r);
                                    if (isSymetrical && !result)
                                        result = Intersects(symetricalCapacityXCenter, capacityYCenter, capacityRadius, r);
                                    return result;
                                }
                        };

                //test line to check that the text avoids the capacity circle
                //var text = new StringBuilder();
                //for (int i = 0; i < 3500; ++i)
                //    text.Append("x ");
                //card.Text = text.ToString();

                const float hMention = 5f;
                float spaceForMention = proxyDocument.NotForLegalPlay ? hMention : 0f;
                WriteTextBlock(doc, cb, xTextBox, yTextBox + spaceForMention, wTextBox, hTextBox - spaceForMention, fonts, card.Text, card.FlavorText, string.Empty, card.Type, card.Type == "Ally", exclusionAreaFuncs);

                if (proxyDocument.NotForLegalPlay)
                {
                    WriteTextCentered(cb, "NOT FOR LEGAL PLAY", xTextBox + wTextBox / 2f, yTextBox, fonts[(int)FontType.Bold], hMention, 0);
                }

                var cardinalFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\quorum_black_bt.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);

                var poolCost = card.PoolCost;
                if (poolCost.Length > 0)
                    DrawPoolCost(doc, x, y, cb, poolCost, "poolcost", ImageExtensions, cardinalFont, 0x000000);

                var bloodCost = card.BloodCost;
                if (bloodCost.Length > 0)
                    DrawBloodCost(doc, x, y, cb, bloodCost, "bloodcost", ImageExtensions, cardinalFont, 0xFFFFFF);

                // write the illustrator name
                var xIllustrator = x + CardWidth * 0.16f;
                var yIllustrator = y + CardHeight * 0.01f;
                WriteText(cb, "Illus: " + card.Illustrator, xIllustrator, yIllustrator, TextFont, 6f, proxyDocument.UseGhost ? 0 : 0xFFFFFF, scaling: 75f);

                // write the copyright
                var xCopyright = x + CardWidth * 0.97f;
                var yCopyright = y + CardHeight * 0.007f;
                WriteRightAlignedText(cb, proxyDocument.Copyright, xCopyright, yCopyright, TextFont, 4f, proxyDocument.UseGhost ? 0 : 0xFFFFFF, scaling: 75f);

                // debug card outlines
                //cb.SetRGBColorStroke(255, 0, 0);
                //cb.Rectangle(x - (CardWidthBorder - CardWidth) / 2f, y - (CardHeightBorder - CardHeight) / 2f, CardWidthBorder, CardHeightBorder);
                //cb.Stroke();
                //cb.SetRGBColorStroke(255, 0, 0);
                //cb.Rectangle(x , y , CardWidth, CardHeight);
                //cb.Stroke();

                ++proxyDocument.ImageIndex;
                if (proxyDocument.ImageIndex % 9 == 0)
                    DrawCutLines(cb);
            }

            outputMessage = outputMessageBuilder.ToString();
        }

        /// <summary>
        /// Write text trying to maximize the font size.
        /// </summary>
        protected void WriteTextBlock(Document doc, PdfContentByte cb,
            float x, float y, float width, float height,
            BaseFont[] fonts, string text, string flavorText, string sphere, string cardType, bool isAlly,
            List<Func<Rectangle, bool, bool>> exclusionAreaFuncs)
        {
            //cb.Rectangle(x, y, width, height);
            //cb.Stroke();

            //get the segments
            var segmentsLines = new List<IList<Segment>>();
            if (sphere.Length > 0)
            {
                var segments = GetSegments(sphere);
                foreach (var segment in segments)
                    //segment.IsCondensed = true;
                    segment.IsBold = true;
                segmentsLines.Add(segments);
            }

            // apply the default styles
            var lines = text.Split('\n');
            var firstLine = true;
            bool isHighlighted = false;
            foreach (var line in lines)
            {
                var segments = GetSegments(line, ref isHighlighted);
                if (segments.Count == 0)
                    continue;

                var firstSegment = segments[0];
                var isKeywordOrCondition = (firstLine && (lines.Length > 1 || cardType == "Ally") && !firstSegment.IsSymbol); // keywords or condition
                bool isSuperiorDisciplineEffect = (!firstLine && firstSegment.IsSymbol && firstSegment.Text[0] == '>'); // superior discipline effect
                var isBold = isKeywordOrCondition || isSuperiorDisciplineEffect;
                if (isBold)
                {
                    foreach (var segment in segments)
                        segment.IsBold = true;
                }
                segmentsLines.Add(segments);
                firstLine = false;
            }

            int lineCount = 0;
            var hasFlavorText = flavorText.Length > 0;
            if (hasFlavorText)
            {
                lines = flavorText.Split('\n');
                lineCount = lines.Length - 1;
                var singleLine = lineCount == 0;
                foreach (var line in lines)
                {
                    var segments = GetSegments(line, ClanIconsDisplay.IgnoreAll, italicizedQuotes: true);
                    foreach (var segment in segments)
                    {
                        segment.IsFlavor = true;
                        if (lineCount > 0 || singleLine)
                            segment.ToggleItalics();
                    }
                    segmentsLines.Add(segments);
                    --lineCount;
                }
            }

            if (isAlly)
                foreach (var line in segmentsLines.Skip(1))
                    if (line[0].IsSymbol)
                        line[0].DrawBackground = true;

            // group the segments
            var segmentsGroupsLines = segmentsLines.Select(segments => GroupSegments(segments.ToList())).ToList();

            // try to make the text fit
            const float minimumSpaceBeforeFlavorFactor = 0.4f;

            var fontSize = Math.Min(MaxTextFontSize, (float)Math.Round(height / lines.Count()));
            bool isFitting = false;
            float remainingVerticalSpace = 0;
            float beforeFlavorSpace;

            while (!isFitting && fontSize > 0)
            {
                // perform a first rendering test to get an idea of the height of the text block. This may change because of the position of the text block
                // if there are any exclusion areas
                float newX, newY;
                GetPositionValues(x, y, width, width, height, hasFlavorText, 0, fontSize, out newX, out newY, out beforeFlavorSpace);
                CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, 0, exclusionAreaFuncs, null);
                GetPositionValues(x, y, width, width, height, hasFlavorText, remainingVerticalSpace, fontSize, out newX, out newY, out beforeFlavorSpace);
                // now perform the test with the position
                isFitting = CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, null)
                    && !(hasFlavorText && remainingVerticalSpace < fontSize * minimumSpaceBeforeFlavorFactor); // we want the flavor text to be separated from the text by a minimum space;

                if (!isFitting)
                {
                    if (fontSize >= 6f)
                        fontSize -= 0.1f;
                    else
                        fontSize -= 0.05f; // try a smaller step
                }
            }
            if (!isFitting)
                throw new Exception("Can't make all the text fit: " + text);

            // try to reduce the number of short lines by reducing the font a little
            var fontSizeTest = fontSize;
            var min = Int32.MaxValue;
            var fontSizeChanged = false;
            var minimumFontSize = fontSize - 0.5;
            while (fontSizeTest >= minimumFontSize)
            {
                // perform a first rendering test to get an idea of the height of the text block. This may change because of the position of the text block
                // if there are any exclusion areas
                float newX, newY;
                GetPositionValues(x, y, width, width, height, hasFlavorText, 0, fontSizeTest, out newX, out newY, out beforeFlavorSpace);
                CountShortLines(segmentsGroupsLines, newX, newY, width, height, fonts, fontSizeTest, 0, exclusionAreaFuncs);
                GetPositionValues(x, y, width, width, height, hasFlavorText, remainingVerticalSpace, fontSizeTest, out newX, out newY, out beforeFlavorSpace);
                // now perform the test with the position
                var count = CountShortLines(segmentsGroupsLines, newX, newY, width, height, fonts, fontSizeTest, beforeFlavorSpace, exclusionAreaFuncs);

                if (count < min)
                {
                    if (min != Int32.MaxValue)
                    {
                        fontSizeChanged = true;
                        fontSize = fontSizeTest;
                    }
                    min = count;
                }
                if (count == 0) // useless to continue
                    break;
                fontSizeTest -= 0.1f;
            }
            // recompute text height
            if (fontSizeChanged)
            {
                // perform a first rendering test to get an idea of the height of the text block. This may change because of the position of the text block
                // if there are any exclusion areas
                float newX, newY;
                GetPositionValues(x, y, width, width, height, hasFlavorText, 0, fontSize, out newX, out newY, out beforeFlavorSpace);
                CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, 0, exclusionAreaFuncs, null);
                GetPositionValues(x, y, width, width, height, hasFlavorText, remainingVerticalSpace, fontSize, out newX, out newY, out beforeFlavorSpace);
                // now perform the test with the position
                CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, null);
            }

            var maxWidth = width;

            // now try to find the smallest text box (without creating any additional line)
            // this will harmonize the lines length (to avoid 1 long line and some short lines)
            // if the remaining vertical space is large, we can afford 1 extra line in some cases (5 lines or more)
            isFitting = false;
            var lastRemainingVerticalSpace = remainingVerticalSpace;
            var lastLineCount = lineCount;
            var maxLineDifference = remainingVerticalSpace / height > 0.4f && lineCount > 4
                ? 1
                : 0;

            width = maxWidth / 2f;
            while (!isFitting && maxWidth >= width)
            {
                // perform a first rendering test to get an idea of the height of the text block. This may change because of the position of the text block
                // if there are any exclusion areas
                float newX, newY;
                GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, 0, fontSize, out newX, out newY, out beforeFlavorSpace);
                CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, 0, exclusionAreaFuncs, null);
                GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, remainingVerticalSpace, fontSize, out newX, out newY, out beforeFlavorSpace);
                // now perform the test with the position
                isFitting = CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, null)
                    && lineCount - lastLineCount <= maxLineDifference // check we don't introduce 1 or too many additional line(s)
                    && !(hasFlavorText && remainingVerticalSpace < fontSize * minimumSpaceBeforeFlavorFactor); // we want the flavor text to be separated from the text by a minimum space
                if (!isFitting)
                    width += 1f;
            }
            if (!isFitting)
            {
                width = maxWidth;
                remainingVerticalSpace = lastRemainingVerticalSpace;
            }
            else if (maxLineDifference != 0 && lineCount - lastLineCount <= maxLineDifference)
            {
                lastRemainingVerticalSpace = remainingVerticalSpace;
                // we introduced a new line, we'll try to increase the font size while no new line is added
                lastLineCount = lineCount;
                fontSizeTest = fontSize;
                while (isFitting)
                {
                    if (fontSizeTest >= 6f)
                        fontSizeTest += 0.1f;
                    else
                        fontSizeTest += 0.05f; // try a smaller step

                    if (fontSizeTest >= MaxTextFontSize)
                        break;

                    // perform a first rendering test to get an idea of the height of the text block. This may change because of the position of the text block
                    // if there are any exclusion areas
                    float newX, newY;
                    GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, 0, fontSizeTest, out newX, out newY, out beforeFlavorSpace);
                    CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSizeTest, 0, out remainingVerticalSpace, out lineCount, 0, exclusionAreaFuncs, null);
                    GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, remainingVerticalSpace, fontSizeTest, out newX, out newY, out beforeFlavorSpace);
                    // now perform the test with the position
                    isFitting = CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, newX, newY, width, height, fonts, fontSizeTest, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, null)
                        && lineCount - lastLineCount == 0
                        && !(hasFlavorText && remainingVerticalSpace < fontSize * minimumSpaceBeforeFlavorFactor); // we want the flavor text to be separated from the text by a minimum space;
                    if (isFitting)
                    {
                        lastRemainingVerticalSpace = remainingVerticalSpace;
                        fontSize = fontSizeTest;
                    }
                }
                remainingVerticalSpace = lastRemainingVerticalSpace;
            }

            // if the card is an ally, try to display the strength and bleed on the second line if it doesn't create any new line
            if (isAlly)
            {
                var header = segmentsLines[0];
                foreach (var segment in header.SkipWhile(s => !s.Text.EndsWith(".")).Skip(1))
                    segment.RequiresFollowingUnbreakableSpace = true;

                var segmentsGroupsLines2 = segmentsLines.Select(segments => GroupSegments(segments.ToList())).ToList();

                float newX, newY;
                float remainingVerticalSpace2;
                GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, 0, fontSize, out newX, out newY, out beforeFlavorSpace);
                CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines2, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace2, out lineCount, 0, exclusionAreaFuncs, null);
                GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, remainingVerticalSpace2, fontSize, out newX, out newY, out beforeFlavorSpace);
                // now perform the test with the position
                isFitting = CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines2, newX, newY, width, height, fonts, fontSize, 0, out remainingVerticalSpace2, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, null)
                    && lineCount - lastLineCount == 0
                    && !(hasFlavorText && remainingVerticalSpace2 < fontSize * minimumSpaceBeforeFlavorFactor); // we want the flavor text to be separated from the text by a minimum space;
                if (isFitting && remainingVerticalSpace2 >= remainingVerticalSpace)
                {
                    remainingVerticalSpace = remainingVerticalSpace2;
                    segmentsGroupsLines = segmentsGroupsLines2;
                }
            }

            // perform the rendering
            GetPositionValues(x, y, maxWidth, width, height, hasFlavorText, remainingVerticalSpace, fontSize, out x, out y, out beforeFlavorSpace);
            CheckIfTextBlockFitsOrWrite(doc, cb, segmentsGroupsLines, x, y, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, new List<float>(), true);
            CheckIfTextBlockFitsOrWrite(doc, cb, segmentsGroupsLines, x, y, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, new List<float>());
        }

        private static void GetPositionValues(float x, float y, float maxWidth, float width, float height, bool hasFlavorText, float remainingVerticalSpace, float fontSize, out float newX, out float newY, out float beforeFlavorSpace)
        {
            beforeFlavorSpace = hasFlavorText ? Math.Min(fontSize, remainingVerticalSpace) : 0;
            remainingVerticalSpace -= beforeFlavorSpace;
            newY = y + height - fontSize - remainingVerticalSpace / 2f;
            newX = x + (maxWidth - width) / 2;
        }

        protected static bool CheckIfTextBlockFitsOrWrite(Document doc, PdfContentByte cb, IList<IList<SegmentGroup>> segmentsGroupsLines,
            float x, float y, float width, float height,
            BaseFont[] fonts, float fontSize, int foreColor,
            out float remainingVerticalSpace, out int lineCount, float beforeFlavorSpace, List<Func<Rectangle, bool, bool>> exclusionAreaFuncs, List<float> segmentWidths, bool renderBackgroundOnly = false)
        {
            remainingVerticalSpace = 0f;
            lineCount = 0;
            var currentHeight = 0f;
            bool firstFlavor = true;
            bool firstLine = true;
            foreach (IList<SegmentGroup> segmentGroups in segmentsGroupsLines)
            {
                if (!segmentGroups[0][0].IsFlavor && !firstLine)
                {
                    currentHeight += fontSize / 6f; // add some space between lines (except flavor which is handled below)
                }
                if (segmentGroups[0][0].IsFlavor && firstFlavor)
                {
                    y -= beforeFlavorSpace;
                    //remainingVerticalSpace -= beforeFlavorSpace;
                    firstFlavor = false;
                }
                float segmentGroupHeight = 0f;
                if (!AppendOrWriteSegments(doc, cb, segmentGroups, x, y - currentHeight, width, ref segmentGroupHeight, fonts, fontSize, isCentered: true, foreColor: foreColor, outlineColor: null, segmentWidths: segmentWidths, renderBackgroundOnly: renderBackgroundOnly, exclusionAreaFuncs: exclusionAreaFuncs, lineCount: ref lineCount)
                    || currentHeight + segmentGroupHeight > height)
                {
                    remainingVerticalSpace = 0f;
                    lineCount = 0;
                    return false;
                }
                currentHeight += segmentGroupHeight;
                firstLine = false;
            }
            remainingVerticalSpace += height - currentHeight;
            return true;
        }

        protected static int CountShortLines(IList<IList<SegmentGroup>> segmentsGroupsLines, float x, float y, float width, float height, BaseFont[] fonts, float fontSize, float beforeFlavorSpace, List<Func<Rectangle, bool, bool>> exclusionAreaFuncs)
        {
            float remainingVerticalSpace;
            int lineCount;
            var widths = new List<float>();
            CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, x, y, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, widths);

            widths.Sort();
            var medianWidth = widths[widths.Count / 2];
            return widths.Count(v => v <= medianWidth / 2);
        }

        protected static float GetLineWidthsVariance(IList<IList<SegmentGroup>> segmentsGroupsLines, float x, float y, float width, float height, BaseFont[] fonts, float fontSize, float beforeFlavorSpace, List<Func<Rectangle, bool, bool>> exclusionAreaFuncs)
        {
            float remainingVerticalSpace;
            int lineCount;
            var widths = new List<float>();
            CheckIfTextBlockFitsOrWrite(null, null, segmentsGroupsLines, x, y, width, height, fonts, fontSize, 0, out remainingVerticalSpace, out lineCount, beforeFlavorSpace, exclusionAreaFuncs, widths);

            var avg = widths.Average();
            var result = widths.Sum(w => (w - avg) * (w - avg));
            return result;
        }

        private static void DrawPoolCost(Document doc, float x, float y, PdfContentByte cb, string cost, string key, string[] imageExtensions, BaseFont cardinalFont, int color)
        {
            var wCost = CardWidth * 0.121f;
            var hCost = CardHeight * 0.162f;
            var costImage = ImageProvider.GetFittingImage("bitmap-" + key, string.Format(@"{0}Images\{1}", ResourcesPath, key), imageExtensions, wCost, hCost);
            var xCost = x + CardWidth * 0.074f;
            var yCost = y + CardHeight * 0.154f;
            //cb.SetRGBColorStroke(255, 0, 0);
            //cb.Rectangle(xCost - wCost/2, yCost - hCost / 2, wCost, hCost);
            //cb.Stroke();
            wCost = costImage.ScaledWidth;
            hCost = costImage.ScaledHeight;

            costImage.SetAbsolutePosition(xCost - wCost / 2, yCost - hCost / 2);
            doc.Add(costImage);

            const float cardinalFontSize = 9f;
            var cardinalWidth = cardinalFont.GetWidthPoint(cost, cardinalFontSize);
            WriteText(cb, cost, xCost - cardinalWidth / 2, yCost - cardinalFontSize * 1.2f, cardinalFont, cardinalFontSize, color);

            //cb.SetRGBColorStroke(255, 0, 0);
            //cb.Rectangle(xCost - cardinalWidth / 2, yCost - cardinalFontSize * 1.2f, cardinalWidth, cardinalFontSize);
            //cb.Stroke();
        }

        private static void DrawBloodCost(Document doc, float x, float y, PdfContentByte cb, string cost, string key, string[] imageExtensions, BaseFont cardinalFont, int color)
        {
            var wCost = CardWidth * 0.095f;
            var hCost = CardHeight * 0.12f;
            var costImage = ImageProvider.GetFittingImage("bitmap-" + key, string.Format(@"{0}Images\{1}", ResourcesPath, key), imageExtensions, wCost, hCost);
            var xCost = x + CardWidth * 0.074f;
            var yCost = y + CardHeight * 0.138f;
            //cb.SetRGBColorStroke(255, 255, 0);
            //cb.Rectangle(xCost - wCost / 2, yCost - hCost / 2, wCost, hCost);
            //cb.Stroke();
            wCost = costImage.ScaledWidth;
            hCost = costImage.ScaledHeight;

            costImage.SetAbsolutePosition(xCost - wCost / 2, yCost - hCost / 2);
            doc.Add(costImage);

            const float cardinalFontSize = 10f;
            var cardinalWidth = cardinalFont.GetWidthPoint(cost, cardinalFontSize);
            WriteText(cb, cost, xCost - cardinalWidth / 2, yCost - cardinalFontSize * 0.775f, cardinalFont, cardinalFontSize, color);

            //cb.SetRGBColorStroke(255, 255, 0);
            //cb.Rectangle(xCost - cardinalWidth / 2, yCost - cardinalFontSize * 0.775f, cardinalWidth, cardinalFontSize);
            //cb.Stroke();
        }

        private class ClanSymbolLayout
        {
            public float Width;
            public Image ClanSymbol;
            public bool IsVerticallyCentered;
            public float X, Y;
        }

        public override void FixCard(StreamWriter outputFile, DataRow cardRow, out string outputMessage)
        {
            var result = new List<string>();
            var card = new LibraryCardData(cardRow);
            outputMessage = "";

            var newData = new Dictionary<string, string>();

            var cardText = cardRow["Card Text"].ToString();
            var openCount = cardText.Count(c => c == '{');
            var closeCount = cardText.Count(c => c == '}');

            if (openCount != closeCount)
                outputMessage += "Mismatch count for { and }.";

            cardText = cardText.Replace("{", null).Replace("}", null);
            if (cardText.Length == 0)
                outputMessage += "Empty card text.";
            else if (!cardText.EndsWith(".") && !cardText.EndsWith(".)"))
                outputMessage += "Card text doesn't end with a mark.";
            if (cardText.Contains("  "))
                outputMessage += "Card text contains multiple contiguous spaces.";

            var lines = cardText.Split('\n');
            if (lines.Length > 1 && lines.Skip(1).Any(l => !l.StartsWith("[") && l.Contains("Only usable")))
                outputMessage += "\"Only usable\" found in card text but not on the first line.";
            if (lines.Length > 1 && lines.Skip(1).Any(l => !l.StartsWith("[") && l.Contains("Usable")))
                outputMessage += "\"Usable\" found in card text but not on the first line.";
            if (lines.Length > 1 && lines.Skip(1).Any(l => !l.StartsWith("[") && l.Contains("Not usable")))
                outputMessage += "\"Not usable\" found in card text but not on the first line.";
            if (lines.Length == 1 && lines[0].Contains("Only usable"))
                outputMessage += "\"Only usable\" should be put on the first line, separated from the other text.";
            if (lines.Length == 1 && lines[0].Contains("Usable"))
                outputMessage += "\"Usable\" should be put on the first line, separated from the other text.";
            if (lines.Length == 1 && lines[0].Contains("Not usable"))
                outputMessage += "\"Not usable\" should be put on the first line, separated from the other text.";

            foreach (var line in lines)
            {
                var onlyUsableLastIndex = line.LastIndexOf("Only usable", StringComparison.InvariantCulture);
                var usableLastIndex = line.LastIndexOf("Usable", StringComparison.InvariantCulture);
                var notUsableLastIndex = line.LastIndexOf("Not usable", StringComparison.InvariantCulture);
                var indices = new[] { onlyUsableLastIndex, usableLastIndex, notUsableLastIndex }.Where(i => i > -1);
                if (!indices.OrderBy(i => i).SequenceEqual(indices))
                    outputMessage += "Order of \"Only usable\", \"Usable\", \"Not usable\" is not correct in card text.";
            }

            if ((cardRow["Type"].ToString() == "Retainer" || cardRow["Type"].ToString() == "Equipment") && cardText.ToLower().Contains("this minion"))
                outputMessage += "Card text contains \"this minion\" that could be replaced by \"bearer\" or \"employer\".";


            if (lines.Length > 1)
            {
                for (int i = 0; i < lines.Length - 1; ++i)
                {
                    var length = lines[i].Length - 1;
                    if (length > 1 && lines[i + 1].Length > length && string.Equals(lines[i].Substring(0, length), lines[i + 1].Substring(0, length), StringComparison.InvariantCultureIgnoreCase))
                        outputMessage += "Card text could use \"As above\".";
                }
            }

            foreach (var column in cardRow.Table.Columns)
            {
                var columnName = column.ToString();
                var value = cardRow[columnName].ToString();

                if (columnName != "Card Text" && columnName != "Requirement" && (value.Contains("{") || value.Contains("}")))
                    outputMessage += "Column " + columnName + " contains errata.";
            }

            foreach (var column in cardRow.Table.Columns)
            {
                var columnName = column.ToString();
                var value = cardRow[columnName].ToString();

                string newValue;
                if (newData.TryGetValue(columnName, out newValue))
                    value = newValue;
                result.Add(value);
            }

            var escapedAndQuotedLine = string.Join(",", result.Select(EscapeAndQuote));
            outputFile.WriteLine(escapedAndQuotedLine);
            if (outputMessage.Length > 0)
                outputMessage = cardRow["Name"] + Environment.NewLine + outputMessage;
        }
    }
}
